msg_tool\scripts\softpal\scr/
mod.rs

1//! Softpal script (.src)
2mod disasm;
3
4use crate::ext::io::*;
5use crate::scripts::base::*;
6use crate::types::*;
7use crate::utils::encoding::*;
8use anyhow::Result;
9use disasm::*;
10use std::collections::HashMap;
11use std::io::{Read, Write};
12
13#[derive(Debug)]
14/// Softpal script builder
15pub struct SoftpalScriptBuilder {}
16
17impl SoftpalScriptBuilder {
18    /// Create a new Softpal script builder
19    pub fn new() -> Self {
20        Self {}
21    }
22}
23
24impl ScriptBuilder for SoftpalScriptBuilder {
25    fn default_encoding(&self) -> Encoding {
26        Encoding::Cp932
27    }
28
29    fn build_script(
30        &self,
31        buf: Vec<u8>,
32        filename: &str,
33        encoding: Encoding,
34        _archive_encoding: Encoding,
35        config: &ExtraConfig,
36        archive: Option<&Box<dyn Script>>,
37    ) -> Result<Box<dyn Script + Send + Sync>> {
38        Ok(Box::new(SoftpalScript::new(
39            buf, filename, encoding, config, archive,
40        )?))
41    }
42
43    fn extensions(&self) -> &'static [&'static str] {
44        &["src"]
45    }
46
47    fn script_type(&self) -> &'static ScriptType {
48        &ScriptType::Softpal
49    }
50
51    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
52        if buf_len >= 4 && buf.starts_with(b"Sv20") {
53            return Some(10);
54        }
55        None
56    }
57}
58
59#[derive(Debug)]
60/// Softpal SRC Script
61pub struct SoftpalScript {
62    data: MemReader,
63    strs: Vec<PalString>,
64    texts: MemReader,
65    encoding: Encoding,
66    label_offsets: Vec<u32>,
67    add_message_index: bool,
68}
69
70impl SoftpalScript {
71    /// Create a new Softpal script
72    pub fn new(
73        buf: Vec<u8>,
74        filename: &str,
75        encoding: Encoding,
76        config: &ExtraConfig,
77        archive: Option<&Box<dyn Script>>,
78    ) -> Result<Self> {
79        let texts = Self::load_texts_data(Self::load_file(filename, archive, "TEXT.DAT")?)?;
80        let points_data = MemReader::new(Self::load_file(filename, archive, "POINT.DAT")?);
81        let label_offsets = Self::load_point_data(points_data)?;
82        let strs = Disasm::new(&buf, &label_offsets)?.disassemble::<MemWriter>(None)?;
83        Ok(Self {
84            data: MemReader::new(buf),
85            strs,
86            encoding,
87            texts,
88            label_offsets,
89            add_message_index: config.softpal_add_message_index,
90        })
91    }
92
93    fn load_file(filename: &str, archive: Option<&Box<dyn Script>>, name: &str) -> Result<Vec<u8>> {
94        if let Some(archive) = archive {
95            Ok(archive
96                .open_file_by_name(name, true)
97                .map_err(|e| anyhow::anyhow!("Failed to open file {} in archive: {}", name, e))?
98                .data()?)
99        } else {
100            let mut path = std::path::PathBuf::from(filename);
101            path.set_file_name(name);
102            path = crate::utils::files::get_ignorecase_path(&path)?;
103            std::fs::read(path).map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", name, e))
104        }
105    }
106
107    fn load_texts_data(data: Vec<u8>) -> Result<MemReader> {
108        let mut writer = MemWriter::from_vec(data);
109        if writer.data.len() >= 0x14 {
110            let ind = writer.cpeek_u32_at(0x10)?;
111            writer.pos = 0x10;
112            if ind != 0 {
113                let mut shift = 4;
114                for _ in 0..(writer.data.len() / 4 - 4) {
115                    let mut data = writer.cpeek_u32()?;
116                    let mut add = data.to_le_bytes();
117                    add[0] = add[0].rotate_left(shift);
118                    shift = (shift + 1) % 8;
119                    data = u32::from_le_bytes(add);
120                    data ^= 0x084DF873 ^ 0xFF987DEE;
121                    writer.write_u32(data)?;
122                }
123            }
124        }
125        Ok(MemReader::new(writer.into_inner()))
126    }
127
128    fn load_point_data(mut data: MemReader) -> Result<Vec<u32>> {
129        let mut magic = [0u8; 16];
130        data.read_exact(&mut magic)?;
131        if magic != *b"$POINT_LIST_****" && magic != *b"_POINT_LIST_****" {
132            return Err(anyhow::anyhow!("Invalid point list magic: {:?}", magic));
133        }
134        let is_crypted = magic[0] == b'$' && {
135            let first_offset = data.cpeek_u32()?;
136            first_offset & 0xFF000000 != 0
137        };
138        let mut label_offsets = Vec::new();
139        let mut shift = 4;
140        while !data.is_eof() {
141            let mut val = data.read_u32()?;
142            if is_crypted {
143                let mut add = val.to_le_bytes();
144                add[0] = add[0].rotate_left(shift);
145                shift = (shift + 1) % 8;
146                val = u32::from_le_bytes(add);
147                val ^= 0x084DF873 ^ 0xFF987DEE;
148            }
149            label_offsets.push(val + CODE_OFFSET);
150        }
151        label_offsets.reverse();
152        Ok(label_offsets)
153    }
154}
155
156impl Script for SoftpalScript {
157    fn default_output_script_type(&self) -> OutputScriptType {
158        OutputScriptType::Json
159    }
160
161    fn default_format_type(&self) -> FormatOptions {
162        FormatOptions::None
163    }
164
165    fn is_output_supported(&self, _: OutputScriptType) -> bool {
166        true
167    }
168
169    fn multiple_message_files(&self) -> bool {
170        true
171    }
172
173    fn extract_messages(&self) -> Result<Vec<Message>> {
174        let mut messages = Vec::new();
175        let mut name = None;
176        let max_len = self.texts.data.len() as u32;
177        for str in &self.strs {
178            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
179            if addr - 4 > max_len {
180                continue;
181            }
182            let idx = self.texts.cpeek_u32_at(addr as u64)?;
183            let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
184            let text =
185                decode_to_string(self.encoding, text.as_bytes(), false)?.replace("<br>", "\n");
186            let text = if self.add_message_index {
187                format!("[{}]{}", idx, text)
188            } else {
189                text
190            };
191            match str.typ {
192                StringType::Name => {
193                    if text.is_empty() {
194                        continue; // Skip empty names
195                    }
196                    name = Some(text);
197                }
198                StringType::Message => messages.push(Message {
199                    name: name.take(),
200                    message: text,
201                }),
202                StringType::Hover => messages.push(Message::new(text, None)),
203                StringType::Label => {} // Ignore labels
204            }
205        }
206        Ok(messages)
207    }
208
209    fn extract_multiple_messages(&self) -> Result<HashMap<String, Vec<Message>>> {
210        let mut hovers = Vec::new();
211        let mut messages = Vec::new();
212        let mut label = None;
213        let mut name = None;
214        let mut result = HashMap::new();
215        let max_len = self.texts.data.len() as u32;
216        for str in &self.strs {
217            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
218            if addr - 4 > max_len {
219                continue;
220            }
221            let idx = self.texts.cpeek_u32_at(addr as u64)?;
222            let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
223            let ptext =
224                decode_to_string(self.encoding, text.as_bytes(), false)?.replace("<br>", "\n");
225            let text = if self.add_message_index {
226                format!("[{}]{}", idx, ptext)
227            } else {
228                ptext.clone()
229            };
230            match str.typ {
231                StringType::Name => {
232                    if text.is_empty() {
233                        continue; // Skip empty names
234                    }
235                    name = Some(text);
236                }
237                StringType::Message => messages.push(Message::new(text, name.take())),
238                StringType::Hover => hovers.push(Message::new(text, None)),
239                StringType::Label => {
240                    if !messages.is_empty() {
241                        let key = label.take().unwrap_or_else(|| "default".to_string());
242                        if result.contains_key(&key) {
243                            eprintln!(
244                                "Warning: Duplicate label '{}', overwriting previous messages.",
245                                key
246                            );
247                            crate::COUNTER.inc_warning();
248                        }
249                        result.insert(key, messages);
250                        messages = Vec::new();
251                    }
252                    label = Some(ptext);
253                }
254            }
255        }
256        if !messages.is_empty() {
257            let key = label.take().unwrap_or_else(|| "default".to_string());
258            result.insert(key, messages);
259        }
260        if !hovers.is_empty() {
261            result.insert("hover".to_string(), hovers);
262        }
263        Ok(result)
264    }
265
266    fn import_messages<'a>(
267        &'a self,
268        messages: Vec<Message>,
269        mut file: Box<dyn WriteSeek + 'a>,
270        filename: &str,
271        encoding: Encoding,
272        replacement: Option<&'a ReplacementTable>,
273    ) -> Result<()> {
274        let mut texts_filename = std::path::PathBuf::from(filename);
275        texts_filename.set_file_name("TEXT.DAT");
276        let mut texts = Vec::new();
277        let mut reader = self.texts.to_ref();
278        reader.pos = 0x10;
279        while !reader.is_eof() {
280            reader.pos += 4; // Skip index
281            texts.push(reader.read_cstring()?)
282        }
283        let mut texts_file = std::fs::File::create(&texts_filename)
284            .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?;
285        file.write_all(&self.data.data)?;
286        let mut mes = messages.iter();
287        let mut mess = mes.next();
288        let texts_data_len = self.texts.data.len() as u32;
289        let mut num_offset_map: HashMap<u32, u32> = HashMap::new();
290        for str in &self.strs {
291            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
292            if addr + 4 > texts_data_len {
293                continue;
294            }
295            if str.typ.is_label() {
296                continue; // Ignore labels
297            }
298            let m = match mess {
299                Some(m) => m,
300                None => return Err(anyhow::anyhow!("Not enough messages.")),
301            };
302            let mut text = match str.typ {
303                StringType::Name => match &m.name {
304                    Some(name) => name.clone(),
305                    None => return Err(anyhow::anyhow!("Missing name for message.")),
306                },
307                StringType::Message => {
308                    let m = m.message.clone();
309                    mess = mes.next();
310                    m
311                }
312                StringType::Hover => {
313                    let m = m.message.clone();
314                    mess = mes.next();
315                    m
316                }
317                StringType::Label => continue, // Ignore labels
318            };
319            if let Some(repl) = replacement {
320                for (from, to) in repl.map.iter() {
321                    text = text.replace(from, to);
322                }
323            }
324            text = text.replace("\n", "<br>");
325            let encoded = encode_string(encoding, &text, false)?;
326            let s = std::ffi::CString::new(encoded)?;
327            let num = texts.len() as u32;
328            num_offset_map.insert(num, str.offset);
329            texts.push(s);
330        }
331        if mess.is_some() || mes.next().is_some() {
332            return Err(anyhow::anyhow!("Some messages were not processed."));
333        }
334        texts_file.write_all(b"$TEXT_LIST__")?;
335        texts_file.write_u32(texts.len() as u32)?;
336        let mut nf = MemWriter::new();
337        for (num, text) in texts.into_iter().enumerate() {
338            let num = num as u32;
339            let newaddr = nf.pos as u32 + 0x10;
340            if let Some(offset) = num_offset_map.get(&num) {
341                file.write_u32_at(*offset as u64, newaddr)?;
342            }
343            nf.write_u32(num)?;
344            nf.write_cstring(&text)?;
345        }
346        nf.pos = 0;
347        let mut shift = 4;
348        for _ in 0..(nf.data.len() / 4) {
349            let mut data = nf.cpeek_u32()?;
350            data ^= 0x084DF873 ^ 0xFF987DEE;
351            let mut add = data.to_le_bytes();
352            add[0] = add[0].rotate_right(shift);
353            shift = (shift + 1) % 8;
354            data = u32::from_le_bytes(add);
355            nf.write_u32(data)?;
356        }
357        texts_file.write_all(&nf.data)?;
358        Ok(())
359    }
360
361    fn import_multiple_messages<'a>(
362        &'a self,
363        messages: HashMap<String, Vec<Message>>,
364        mut file: Box<dyn WriteSeek + 'a>,
365        filename: &str,
366        encoding: Encoding,
367        replacement: Option<&'a ReplacementTable>,
368    ) -> Result<()> {
369        let mut texts_filename = std::path::PathBuf::from(filename);
370        texts_filename.set_file_name("TEXT.DAT");
371        let mut texts = Vec::new();
372        let mut reader = self.texts.to_ref();
373        reader.pos = 0x10;
374        while !reader.is_eof() {
375            reader.pos += 4; // Skip index
376            texts.push(reader.read_cstring()?)
377        }
378        let mut texts_file = std::fs::File::create(&texts_filename)
379            .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?;
380        file.write_all(&self.data.data)?;
381        let hover_messages = messages.get("hover").cloned().unwrap_or_default();
382        let mut hover_iter = hover_messages.iter();
383        let mut hover_mes = hover_iter.next();
384        let mut cur_label: Option<String> = None;
385        let mut cur_messages = messages
386            .get(cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default"))
387            .cloned()
388            .unwrap_or_default();
389        let mut cur_iter = cur_messages.iter();
390        let mut cur_mes = cur_iter.next();
391        let texts_data_len = self.texts.data.len() as u32;
392        let mut num_offset_map: HashMap<u32, u32> = HashMap::new();
393        for str in &self.strs {
394            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
395            if addr + 4 > texts_data_len {
396                continue;
397            }
398            let mut text = match str.typ {
399                StringType::Label => {
400                    if cur_mes.is_some() || cur_iter.next().is_some() {
401                        return Err(anyhow::anyhow!(
402                            "Not all messages were used for label {}.",
403                            cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
404                        ));
405                    }
406                    let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
407                    let text = decode_to_string(self.encoding, text.as_bytes(), false)?
408                        .replace("<br>", "\n");
409                    cur_messages = messages.get(text.as_str()).cloned().unwrap_or_default();
410                    cur_iter = cur_messages.iter();
411                    cur_mes = cur_iter.next();
412                    cur_label = Some(text);
413                    // We don't need update labels
414                    continue;
415                }
416                StringType::Hover => {
417                    let m = match hover_mes {
418                        Some(m) => m,
419                        None => return Err(anyhow::anyhow!("Not enough hover messages.")),
420                    };
421                    let m = m.message.clone();
422                    hover_mes = hover_iter.next();
423                    m
424                }
425                StringType::Name => {
426                    let m = match cur_mes {
427                        Some(m) => m,
428                        None => {
429                            return Err(anyhow::anyhow!(
430                                "Not enough messages for label {}.",
431                                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
432                            ));
433                        }
434                    };
435                    let name = match &m.name {
436                        Some(name) => name.clone(),
437                        None => return Err(anyhow::anyhow!("Missing name for message.")),
438                    };
439                    name
440                }
441                StringType::Message => {
442                    let m = match cur_mes {
443                        Some(m) => m,
444                        None => {
445                            return Err(anyhow::anyhow!(
446                                "Not enough messages for label {}.",
447                                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
448                            ));
449                        }
450                    };
451                    let m = m.message.clone();
452                    cur_mes = cur_iter.next();
453                    m
454                }
455            };
456            if let Some(repl) = replacement {
457                for (from, to) in repl.map.iter() {
458                    text = text.replace(from, to);
459                }
460            }
461            text = text.replace("\n", "<br>");
462            let encoded = encode_string(encoding, &text, false)?;
463            let s = std::ffi::CString::new(encoded)?;
464            let num = texts.len() as u32;
465            num_offset_map.insert(num, str.offset);
466            texts.push(s);
467        }
468        if cur_mes.is_some() || cur_iter.next().is_some() {
469            return Err(anyhow::anyhow!(
470                "Some messages were not processed for label {}.",
471                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
472            ));
473        }
474        if hover_mes.is_some() || hover_iter.next().is_some() {
475            return Err(anyhow::anyhow!("Some hover messages were not processed."));
476        }
477        texts_file.write_all(b"$TEXT_LIST__")?;
478        texts_file.write_u32(texts.len() as u32)?;
479        let mut nf = MemWriter::new();
480        for (num, text) in texts.into_iter().enumerate() {
481            let num = num as u32;
482            let newaddr = nf.pos as u32 + 0x10;
483            if let Some(offset) = num_offset_map.get(&num) {
484                file.write_u32_at(*offset as u64, newaddr)?;
485            }
486            nf.write_u32(num)?;
487            nf.write_cstring(&text)?;
488        }
489        nf.pos = 0;
490        let mut shift = 4;
491        for _ in 0..(nf.data.len() / 4) {
492            let mut data = nf.cpeek_u32()?;
493            data ^= 0x084DF873 ^ 0xFF987DEE;
494            let mut add = data.to_le_bytes();
495            add[0] = add[0].rotate_right(shift);
496            shift = (shift + 1) % 8;
497            data = u32::from_le_bytes(add);
498            nf.write_u32(data)?;
499        }
500        texts_file.write_all(&nf.data)?;
501        Ok(())
502    }
503
504    fn custom_output_extension<'a>(&'a self) -> &'a str {
505        "txt"
506    }
507
508    fn custom_export(&self, filename: &std::path::Path, _encoding: Encoding) -> Result<()> {
509        let file = std::fs::File::create(filename)
510            .map_err(|e| anyhow::anyhow!("Failed to create file {}: {}", filename.display(), e))?;
511        let mut file = std::io::BufWriter::new(file);
512        Disasm::new(&self.data.data, &self.label_offsets)?.disassemble(Some(&mut file))?;
513        Ok(())
514    }
515}